﻿using System;
using System.Collections;
using System.Collections.Generic;
using Dorm.Helpers;
using Dorm.Maps;
using Dorm.QueryObject;

namespace Dorm.Services
{
    public class SaveService<T>
    {
        protected DataAccess DataAccess { get; set; }

        public SaveService(DataAccess dataAccess) { DataAccess = dataAccess; }

        public bool MapAndRun(T entity)
        {
            bool success;
            var entityInfo = new EntityInfo(entity);
            bool useImplicitTransaction = DataAccess.HasValidTransaction == false;

            try
            {
                if (useImplicitTransaction)
                    DataAccess.BeginTransaction();

                if (entityInfo.IsNew())
                    success = ManageEntityInsert(entityInfo);
                else
                    success = ManageEntityUpdate(entityInfo);

                if (useImplicitTransaction)
                    DataAccess.CommitTransaction();
            }
            catch (Exception)
            {
                if (useImplicitTransaction)
                    DataAccess.RollbackTransaction();

                throw;
            }

            return success;
        }

        bool ManageEntityInsert(EntityInfo entityInfo)
        {
            var entityMaps = MapLocator.GetMap(entityInfo.EntityType);

            var insertEntity = new Insert().Into(entityInfo.TypeName).Values(entityInfo.InnerEntity).ReturnID();
            entityInfo.ID = Convert.ToInt64(DataAccess.ExecuteScalar(insertEntity.ToString()));

            foreach (var propertyToCascade in entityMaps.FindAll(m => m.Cascade))
            {
                if (entityInfo.GetValue(propertyToCascade.PropertyName) == null)
                    continue;

                foreach (var child in entityInfo.GetValue(propertyToCascade.PropertyName) as IEnumerable)
                {
                    var childInfo = new EntityInfo(child);

                    entityInfo.BindParentReference(child);
                    
                    if (propertyToCascade.MappingType == MappedProperty.MappingTypes.HasMany)
                    {
                        ManageEntityInsert(childInfo);                        
                    }
                    else if (propertyToCascade.MappingType == MappedProperty.MappingTypes.HasManyToMany)
                    {
                        string manyToManyTableName = MappedProperty.GetManyToManyTableName(entityInfo.TypeName, propertyToCascade.PropertyType.Name);
                        var insertRelation = new Insert().Into(manyToManyTableName)
                                                .Value(entityInfo.TypeName + "ID", entityInfo.ID)
                                                .Value(childInfo.TypeName + "ID", childInfo.ID);
                        DataAccess.ExecuteNonQuery(insertRelation.ToString());
                    }
                }                
            }

            return entityInfo.ID > 0;
        }

        private bool ManageEntityUpdate(EntityInfo entityInfo)
        {
            var entityMaps = MapLocator.GetMap(entityInfo.EntityType);

            var updateEntity = new Update(entityInfo.TypeName).Set(entityInfo.InnerEntity);
            bool success = DataAccess.ExecuteNonQuery(updateEntity.ToString()) > 0;

            foreach (var propertyToCascade in entityMaps.FindAll(m => m.Cascade))
            {
                if (entityInfo.GetValue(propertyToCascade.PropertyName) == null)
                    continue;

                var newChildList = (IList)entityInfo.GetValue(propertyToCascade.PropertyName);

                MergeCurrentAndNewList(entityInfo, propertyToCascade, newChildList);
            }

            return success;
        }

        private void MergeCurrentAndNewList(EntityInfo entityInfo, MappedProperty childProperty, IList newChildList)
        {
            var mergedList = new List<long>();

            string manyToManyTableName = MappedProperty.GetManyToManyTableName(entityInfo.TypeName, childProperty.PropertyType.Name);
            if (childProperty.MappingType == MappedProperty.MappingTypes.HasManyToMany)
            {
                var deleteRelations = new Delete().From(manyToManyTableName)
                        .Where(entityInfo.TypeName + "ID = " + entityInfo.ID);

                DataAccess.ExecuteNonQuery(deleteRelations.ToString());
            }

            foreach (var newChild in newChildList)
            {
                var newChildInfo = new EntityInfo(newChild);

                if (childProperty.MappingType == MappedProperty.MappingTypes.HasMany)
                {
                    entityInfo.BindParentReference(newChild);

                    if (!newChildInfo.IsNew())
                        ManageEntityUpdate(newChildInfo);
                    else
                        ManageEntityInsert(newChildInfo);
                }

                else if (childProperty.MappingType == MappedProperty.MappingTypes.HasManyToMany)
                {
                    var insertRelation = new Insert().Into(manyToManyTableName)
                        .Value(entityInfo.TypeName + "ID", entityInfo.ID)
                        .Value(newChildInfo.TypeName + "ID", newChildInfo.ID);
                    DataAccess.ExecuteNonQuery(insertRelation.ToString());
                }

                mergedList.Add(newChildInfo.ID);
            }


            if (childProperty.MappingType == MappedProperty.MappingTypes.HasMany)
            {
                var childMaps = MapLocator.GetMap(childProperty.PropertyType);
                var parentReferenceProperty = childMaps.Find(m => m.PropertyType == entityInfo.EntityType);
                string selectCurrentChildren = "SELECT * FROM " + childProperty.PropertyType.Name + " WHERE " + parentReferenceProperty.PropertyName + "ID = " + entityInfo.ID;

                var currentChildList = (IList)Activator.CreateInstance(newChildList.GetType());
                currentChildList = new DbToEntityMapper().Map(childProperty.PropertyType, currentChildList,
                                                              DataAccess.ExecuteReader(selectCurrentChildren));

                foreach (var currentChild in currentChildList)
                {
                    var currentChildInfo = new EntityInfo(currentChild);
                    if (!mergedList.Contains(currentChildInfo.ID))
                    {
                        new DeleteService(DataAccess).MapAndRun(currentChild);
                    }
                }
            }
        }
    }
}
